UNPKG

aliaset

Version:
475 lines (377 loc) 13.8 kB
import { error, invalid } from '@sveltejs/kit' import { normalizeImportMap } from '@jsenv/importmap' import { Semver } from 'sver' import { dev } from '$app/environment' import { env } from '$env/dynamic/private' import { loadDefaultTemplate, loadTemplate } from '$lib/templates' import { toBase64, toBase64url } from '$lib/base64' import type { Manifest, Workspace } from '$lib/types' import pkg from 'lz-string' const { decompressFromEncodedURIComponent, compressToEncodedURIComponent } = pkg import { version as SITE_VERSION } from '../../../package.json' const MANIFEST_PATH = '/_app/cdn.json' const HOSTNAME = 'twind-run.pages.dev' // interface PageData { // manifests: { // latest: Manifest | undefined // next: Manifest | undefined // canary: Manifest | undefined // } // } import wordlistraw from './wordlist.txt?raw' // Based on // - https://gist.github.com/fogleman/c4a1f69f34c7e8a00da8 // - https://www.eff.org/files/2016/09/08/eff_short_wordlist_1.txt from https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases // 4547 words that are 3-5 chars and good candidates for a prefix search const wordlist = wordlistraw.trim().split(/\s+/g) // content adressable store // twind.run/<base64url(integrity)> -> twind.run/Ttj8VsbHHK9N0k1KS94JH_RYFxFwOQ6D7hFg3XUnSTw // actions: share // after login named/aliased urls with default to three named path generated from integrity // actions: save (same scope), fork (different scope), share // properties: title, description, slug // twind.run/~<user>/<alias> -> twind.run/~sastan/brother-santa-bruno // twind.run/@<team>/<alias> -> twind.run/@twind/brother-santa-bruno // Metadata of up to 1024 bytes per key const EXPECTED_VERSION = '1' import { z } from 'zod' const workspaceFileSchema = z.object({ path: z.string().min(1), value: z.string(), }) const workspaceSchema = z.object({ version: z.string().min(1), html: workspaceFileSchema, script: workspaceFileSchema, config: workspaceFileSchema, }) export async function load({ request, params: { key }, platform: { env, caches }, fetch, }: Parameters<import('./$types').PageServerLoad>[0]) { const { workspace } = await loadWorkspace() // default no version: use version from local manifest // specific version: load manifest from v1-0-0.twind-run.pages.dev // dist tag: load manifest from twind-run.pages.dev for 'latest' or dist-tag.twind-run.pages.dev const [localManifest, workspaceManifest, latestManifest, nextManifest] = await Promise.all([ loadManifest(SITE_VERSION), loadManifest(workspace?.version).catch(() => null), loadManifest('latest').catch(() => null), loadManifest('next').catch(() => null), ]) workspace.version = workspaceManifest?.version || '' if ( !( workspace.version && [ localManifest.version, workspaceManifest?.version, latestManifest?.version, nextManifest?.version, ].includes(workspace.version) ) ) { workspace.version ||= localManifest.version } const manifests = [localManifest, workspaceManifest, latestManifest, nextManifest] // remove nullish (not found) .filter(<T>(x: T): x is NonNullable<T> => x != null) // remove duplicate versions .filter( ({ version }, index, source) => source.findIndex((other) => other.version === version) === index, ) // sort in reverse order: latest, next, canary // order: latest (current release), next (upcoming release), canary (PR preview), .sort((a, b) => Semver.compare(b.version, a.version)) // console.debug({ workspace, manifests }) return { workspace, manifests } async function loadWorkspace(): Promise<{ workspace: Workspace }> { if (!key) { return loadDefaultTemplate() } const template = await loadTemplate(key) if (template) { return template } const data = await env.WORKSPACES.get(key).catch((error) => { if (error.message.includes('(10020)')) { // get: The specified object name is not valid. (10020) // maybe a lz-string encoded url return null } if (error.message.includes('(414)')) { // get: UTF-8 encoded length of 1747 exceeds key length limit of 1024. (414) // maybe a lz-string encoded url return null } throw error }) if (!data) { const data = decompressFromEncodedURIComponent(key) if (!data) { throw error(404, 'Not found') } if (!data.startsWith(`${EXPECTED_VERSION}:`)) { // TODO: better error message throw error(404, 'Not found') } const workspace = JSON.parse(data.slice(`${EXPECTED_VERSION}:`.length)) const result = workspaceSchema.safeParse(workspace) if (!result.success) { throw error(400, 'invalid workspace') } return { workspace: result.data } } if (data.httpMetadata?.contentType !== 'application/json') { // TODO: better error message throw error(404, 'Not found') } if (data.customMetadata?.version !== EXPECTED_VERSION) { throw error(404, 'Not found') } if (data.httpMetadata.contentEncoding) { if (data.httpMetadata.contentEncoding !== 'gzip') { // TODO: better error message throw error(404, 'Not found') } const blob = await toBlob(data.body.pipeThrough(await createGunzipStream()), { type: 'application/json', }) return { workspace: JSON.parse(await blob.text()) } } return { workspace: await data.json() } } async function loadManifest(version: string): Promise<Manifest> { // Branch name aliases are lowercased and non-alphanumeric characters are replaced with a hyphen const alias = version === '*' ? 'latest' : version.toLowerCase().replace(/[^a-z\d]/g, '-') const origin = version === SITE_VERSION ? new URL(request.url).origin : alias === 'latest' ? `https://${HOSTNAME}` : `https://${/^\d\.\d\.\d/.test(alias) ? 'v' + alias : alias}.${HOSTNAME}` const url = origin + MANIFEST_PATH const cache = caches?.default let response = await cache?.match(url) if (!response) { console.debug(`Fetching CDN manifest for ${alias || '<empty>'} (${origin})`) response = await fetch(url) if (!(response.ok && response.status === 200)) { throw new Error(`[${response.status}] ${response.statusText || 'request failed'}`) } cache?.put(url, response.clone()) } const manifest = await response.json<Manifest>() if (manifest.version !== version) { try { console.debug( `Resolving CDN manifest for ${alias || '<empty>'} (${origin}) using ${manifest.version}`, ) return await loadManifest(manifest.version) } catch { console.warn(`Failed to fetch CDN manifest for ${manifest.version}`) } } console.debug(`Loaded CDN manifest for ${alias || '<empty>'} (${origin})`) return { ...manifest, ...normalizeImportMap(manifest, response.url), url: response.url, } } } export const actions: import('./$types').Actions = { share: async ({ request, platform }) => { try { const body = await request.formData() const token = body.get('cf-turnstile-response') if (!token) { return invalid(400, { missing: 'turnstile' }) } const ip = request.headers.get('CF-Connecting-IP') // Validate the token by calling the // "/siteverify" API endpoint. const trunstileData = new FormData() trunstileData.append('secret', env.TURNSTILE_SECRET) trunstileData.append('response', token) if (ip) { trunstileData.append('remoteip', ip) } const turnstileResult = await fetch( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: trunstileData, }, ) const outcome: { success: boolean challenge_ts: string hostname: string 'error-codes': string[] action?: string cdata?: string } = await turnstileResult.json() if (!(outcome.success && (dev || outcome.action === 'share'))) { return invalid(400, outcome) } // TODO: ensure the request is valid // TODO: ensure max request size is 512kb??? const version = body.get('version') if (version !== EXPECTED_VERSION) { return invalid(400, { version: 'mismatch' }) } const workspaceRaw = body.get('workspace') if (!workspaceRaw) { return invalid(400, { workspace: 'missing' }) } if (typeof workspaceRaw !== 'string') { return invalid(400, { workspace: 'invalid' }) } const result = workspaceSchema.safeParse(JSON.parse(workspaceRaw)) if (!result.success) { return invalid(400, { workspace: 'invalid', error: result.error.format() }) } const blob = new Blob([JSON.stringify(result.data)], { type: 'application/json' }) try { const { key, integrity, missing } = await generate(platform, await blob.arrayBuffer()) // not found if (missing) { // This dance is the only way I could get it work const value = await toBlob(blob.stream().pipeThrough(await createGzipStream())) const { readable, writable } = typeof FixedLengthStream === 'function' ? new FixedLengthStream(value.size) : { readable: value, writable: null } await Promise.all([ writable && value.stream().pipeTo(writable), platform.env.WORKSPACES.put(key, readable, { customMetadata: { version, integrity }, httpMetadata: { contentType: 'application/json', contentEncoding: 'gzip', }, }), ]) } return { success: true, key, inserted: missing } } catch (error) { return { success: true, key: compressToEncodedURIComponent(version + ':' + (await blob.text())), message: (error as Error).message, code: (error as any).code, stack: (error as Error).stack, } } } catch (error) { return invalid(500, { message: (error as Error).message, code: (error as any).code, stack: (error as Error).stack, }) } }, } async function generate(platform: App.Platform, source: BufferSource) { const sha256 = await crypto.subtle.digest('SHA-512', source) const integrity = toBase64(sha256) const view = new DataView(sha256) // try different slugs for (let i = 0; i < view.byteLength - 6; i += 2) { const key = [ // 2685 * 2685 * 2685 = 19.356.769.125 // <word>-<word>-<word> wordlist[view.getUint16(i) % wordlist.length], wordlist[view.getUint16((i += 2)) % wordlist.length], wordlist[view.getUint16((i += 2)) % wordlist.length], ].join('-') const existing = await platform.env.WORKSPACES.head(key) // does not exist or it is the same data if ( !existing || (existing.customMetadata?.integrity && existing.customMetadata.integrity === integrity) ) { return { key, integrity, missing: !existing } } } // fallback to short readable integrity { const key = integrity .toLowerCase() // no vowels or similiar looking chars .replace(/[=+/01aefijlout]/g, '') .slice(-12) .replace(/(.{4})(?!$)/g, '$1-') const existing = await platform.env.WORKSPACES.head(key) // does not exist or it is the same data if ( !existing || (existing.customMetadata?.integrity && existing.customMetadata.integrity === integrity) ) { return { key, integrity, missing: !existing } } } // fallback to long readable integrity { const key = integrity .toLowerCase() // no vowels or similiar looking chars .replace(/[=+/01aefijlout]/g, '') .slice(-25) .replace(/(.{5})(?!$)/g, '$1-') const existing = await platform.env.WORKSPACES.head(key) // does not exist or it is the same data if ( !existing || (existing.customMetadata?.integrity && existing.customMetadata.integrity === integrity) ) { return { key, integrity, missing: !existing } } } // fallback to full integrity const key = toBase64url(integrity) const existing = await platform.env.WORKSPACES.head(key) return { key, integrity, missing: !existing } } async function createGzipStream(): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> { if (dev && typeof CompressionStream !== 'function') { const zlib = await import('node:zlib') const gzip = zlib.createGzip({ level: zlib.constants.Z_BEST_COMPRESSION }) const stream = await import('node:stream') return { writable: stream.Writable.toWeb(gzip), readable: stream.Readable.toWeb(gzip), } } return new CompressionStream('gzip') } async function createGunzipStream(): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> { if (dev && typeof CompressionStream !== 'function') { const zlib = await import('node:zlib') const gzip = zlib.createGunzip() const stream = await import('node:stream') return { writable: stream.Writable.toWeb(gzip), readable: stream.Readable.toWeb(gzip), } } return new DecompressionStream('gzip') } async function toBlob( readable: ReadableStream<BlobPart>, options?: BlobPropertyBag, ): Promise<Blob> { const parts: BlobPart[] = [] await readable.pipeTo( new WritableStream({ write(chunk) { parts.push(chunk) }, }), ) return new Blob(parts, options) }